﻿using System;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using MLSharp.Utilities;

namespace MLSharp.See5
{
	/// <summary>
	/// Provides access to the See5 induction application.  Actual
	/// classifier factories are built on top of this.
	/// </summary>
	public abstract class See5
	{
		#region Logger
		
		/// <summary>
		/// The logger for the class.
		/// </summary>
		private static readonly log4net.ILog mLogger =
			log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);

		#endregion

		#region Private Fields

		/// <summary>
		/// The full path to the see5x.exe executable.
		/// </summary>
		private readonly string mSee5XPath;

		/// <summary>
		/// The full path to the directory to place See5 output
		/// and intermediate files.
		/// </summary>
		private readonly string mSee5WorkingDirectory;

		/// <summary>
		/// The path to the See5 "See5Sam.exe" program used for performing
		/// classifications with a generated tree.
		/// </summary>
		private readonly string mSee5SamPath;

		/// <summary>
		/// The See5 options.
		/// </summary>
		private readonly See5Options mOptions;

		#endregion

		#region Private Methods

		/// <summary>
		/// Validates the executable paths.  This method does not check that
		/// the target exes are the *correct* exes, only that they exist.
		/// </summary>
		private void ValidateConfiguration()
		{
			if (!File.Exists(mSee5XPath))
			{
				throw (new InvalidOperationException("See5X not found at path " + mSee5XPath));
			}

			if (!File.Exists(mSee5SamPath))
			{
				throw (new InvalidOperationException("See5Sam not found at path " + mSee5SamPath));
			}
		}

		/// <summary>
		/// Trains See5 using the specified options.
		/// </summary>
		/// <param name="trainingData">The training data.</param>
		/// <param name="buildRulesets">True to build rulesets, false to build a decision tree (the default).</param>
		/// <param name="stemName">This will be the file stem name used by See5.</param>
		/// <param name="targetAttributeName">This will be the name of the target attribute used by See5.</param>
		protected void TrainSee5(IDataSet trainingData, bool buildRulesets,  out string stemName, out string targetAttributeName)
		{
			targetAttributeName = trainingData.Attributes[trainingData.TargetAttributeIndex].Name;

			//Construct a random string to use as the stem name.
			stemName = NameGenerator.GetRandomName(10);

			//First, write the See5 data to the working directory.
			See5DataWriter writer = new See5DataWriter(stemName, targetAttributeName);
			writer.WriteNamesAndData(trainingData, mSee5WorkingDirectory, mOptions.IgnoredAttributeIndexes);

			//Prepare to run See5 in that directory.
			ProcessStartInfo see5Info = new ProcessStartInfo();
			see5Info.FileName = "cmd.exe";
			see5Info.WorkingDirectory = mSee5WorkingDirectory;
			if (buildRulesets)
			{
				see5Info.Arguments = string.Format("/c \"{0}\" -f {1}{2}{3} > {1}.output", mSee5XPath, stemName, mOptions.ToCommandString(), " -r");
			}
			else
			{
				see5Info.Arguments = string.Format("/c \"{0}\" -f {1}{2} > {1}.output", mSee5XPath, stemName, mOptions.ToCommandString());
			}

			see5Info.UseShellExecute = false;
			see5Info.CreateNoWindow = true;

			//Now that everything is in place and configured, run See5...
			Process see5Process;

			try
			{
				see5Process = Process.Start(see5Info);

				if (see5Process == null)
					throw new NullReferenceException("Process.Start returned null.");
			}
			catch (Exception ex)
			{
				throw (new ApplicationException("Unable to start See5 at " + mSee5XPath, ex));
			}

			see5Process.WaitForExit();

			string output = File.ReadAllText(Path.Combine(mSee5WorkingDirectory, stemName + ".output"));

			//If See5 did not terminate with the expected output or if it didn't generate a tree
			if (!output.EndsWith("secs\r\n") || 
				!(File.Exists(Path.Combine(mSee5WorkingDirectory, stemName + ".tree")) || 
				  File.Exists(Path.Combine(mSee5WorkingDirectory, stemName + ".rules"))
				  )
			    )
			{
				if (mLogger.IsErrorEnabled)
				{
					mLogger.Error("Unexpected output from See5, dumping output:");
					mLogger.Error("--------------------------------------------");
					mLogger.Error(output);
				}

				throw new InvalidOperationException("Unexpected output from See5.");
			}

			//Clean up the .data and .output files since they aren't needed anymore, 
			//but leave the other files. They'll be cleaned by the tree when it is disposed.
			File.Delete(Path.Combine(mSee5WorkingDirectory, stemName + ".data"));
			File.Delete(Path.Combine(mSee5WorkingDirectory, stemName + ".output"));
		}

		#endregion

		#region Protected Properties

		/// <summary>
		/// Gets the See5Sam Path.
		/// </summary>
		protected string See5SamPath
		{
			get { return mSee5SamPath; }
		}

		/// <summary>
		/// Gets the See5X Path.
		/// </summary>
		protected string See5XPath
		{
			get { return mSee5XPath; }
		}

		/// <summary>
		/// The working directory to use.
		/// </summary>
		protected string See5WorkingDirectory
		{
			get { return mSee5WorkingDirectory; }
		}

		#endregion

		#region Public Properties

		/// <summary>
		/// The options to use when constructing new classifiers.
		/// </summary>
		public See5Options Options
		{
			get
			{
				return mOptions;
			}
		}

		#endregion

		#region Public Constructors

		/// <summary>
		/// Instantiates and initializes See5.  
		/// </summary>
		/// <remarks>
		/// You can configure See5 using the app.config file.  You can specify:
		/// <list>
		///		<item>See5XPath - the path to See5X.exe.</item>
		///		<item>See5WorkingDirectory - A directory to place See5 intermediate output into.</item>
		///		<item>See5SamPath - The path to See5Sam.exe.</item>
		/// </list>
		/// </remarks>
		protected See5() : this(new See5Options())
		{
		}

		/// <summary>
		/// Creates a factory using See5 paths specified by the application
		/// configuration file and the specified construction options.
		/// </summary>
		/// <param name="options">The options.</param>
		protected See5(See5Options options)
		{
			mOptions = options;

			//Try to read settings from the app configuration, otherwise fall
			//back and use defaults.
			mSee5XPath = ConfigurationManager.AppSettings["See5XPath"] ?? Path.Combine(Environment.CurrentDirectory, "see5x.exe");

			mSee5SamPath = ConfigurationManager.AppSettings["See5SamPath"] ?? Path.Combine(Environment.CurrentDirectory, "see5sam.exe");

			mSee5WorkingDirectory = ConfigurationManager.AppSettings["See5WorkingDirectory"] ?? Path.GetFullPath(@".\See5");

			if (!Directory.Exists(mSee5WorkingDirectory))
			{
				Directory.CreateDirectory(mSee5WorkingDirectory);
			}

			ValidateConfiguration();
		}

        /// <summary>
        /// Instantiates an initializes See5 by overriding the default
        /// or app.config settings.
        /// </summary>
        /// <param name="see5XPath">The path to the See5X executable.</param>
        /// <param name="see5SamPath">The path to the See5Sam executable.</param>
        /// <param name="workingDirectory">The directory to use for intermediate and output files.</param>
        protected See5(string see5XPath, string see5SamPath, string workingDirectory)
        {
			mOptions = new See5Options();

            mSee5XPath = Path.GetFullPath(see5XPath);
        	mSee5SamPath = Path.GetFullPath(see5SamPath);
            mSee5WorkingDirectory = Path.GetFullPath(workingDirectory);

        	ValidateConfiguration();
        }

		#endregion
	}
}
